/**
 * Experimental drag functionality, bound to the DOM class "item".
 */
(function($) {
	"use strict";

	// Object model for storing data about the currently selected item:
	var _current = {

		// data fields encapsulated by this object:
		_data : {
			object : undefined,
			placeholder : undefined,
			proxy : undefined,
			// Data structure containing knowledge about mouse click positions:
			mousedown : undefined,
			mouseup : undefined,
			origin : undefined,
			anchor : undefined,
			offset : undefined
		},
		// returns the stored place-holder element for the current item:
		getPlaceholder : function() {
			return _current._data.placeholder;
		},
		// stores the specified object as the new place-holder item:
		setPlaceholder : function(item) {
			_current._data.placeholder = item;
		},
		// returns the stored proxy element for the current item:
		getProxy : function() {
			return _current._data.proxy;
		},
		// stores the specified object as the new proxy item:
		setProxy : function(item) {
			_current._data.proxy = item;
		},
		// returns the stored current item:
		get : function() {
			return _current._data.object;
		},
		// stores the specified object as the new current item:
		set : function(item) {
			_current._data.object = item;
		},
		// returns true if and only if the current item is defined:
		isSet : function() {
			return _current._data.object !== undefined;
		}
	},

	// Shorthand function for accessing and manipulating the current item:
	current = function(i) {
		var result, args;
		if (i) {
			if (typeof i === "string") {
				if (i === "clear") {
					current("set", undefined);

				} else if (i === "clearProxy") {
					current("setProxy", undefined);

				} else if (i === "clearPlaceholder") {
					current("setPlaceholder", undefined);

				} else if (typeof _current[i] === "function") {
					args = Array.prototype.slice.call(arguments, 1);
					result = _current[i].apply(this, args);
					return result;
				} else {
					console.error('The method ' + i + ' does not exist.');
				}
			} else if (typeof i === "object")
				_current.set(i);
			else
				console.error(i, "is not a valid argument.");
		}
		return _current.get();
	},

	fn = {
		// Local implementation of experimental item selection functionality.
		select : function(item, _switch) {
			var that = (item.toggleClass) ? item : $(item);
			if (that.hasClass("selected") !== _switch) {
				that.toggleClass("selected");
			}
		},

		// Convenient function for constructing Cartesian XY pairs. Can be used
		// to express planar (two-dimensional) geometric points such as
		// coordinates, or the dimensions of squares and rectangles.
		cartesian : function(x_Value, y_Value) {
			return {
				X : x_Value,
				Y : y_Value
			};
		},

		// Calculates the "hypotenuse" (the diagonal intersection of a
		// rectangle) given the lengths of the two sides of a right-angled
		// triangle or quadrilateral.
		evalHyptnuse : function(x, y) {
			return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
		},

		// Calculates the linear distance between two Cartesian XY points.
		// Linear distance is synonymous with geometric displacement (the
		// absolute magnitude that represents the distance between between the
		// two points).
		linearDist : function(pointA, pointB) {
			var dimensions = fn.planarDist(pointA, pointB);
			return fn.evalHyptnuse(dimensions.X, dimensions.Y);
		},

		// Calculates the planar distance between two Cartesian XY points.
		planarDist : function(pointA, pointB) {
			return fn.cartesian(pointA.X - pointB.X, pointA.Y - pointB.Y);
		},

		// Calculates the absolute planar distance between two Cartesian points
		absPlanarDist : function(a, b) {
			return fn.cartesian(Math.abs(a.X - b.X), Math.abs(a.Y - b.Y));
		}
	};

	// public methods exposed via the plug-in API.
	var methods = {
		_bind : function(elements) {
			var that = $(elements);
			that.on("mousedown.dragndrop", ".item.draggable", function(e) {
				if (!e.isDefaultPrevented()) {
					e.preventDefault();
				}
				var that, cursor, origin;
				that = $(this);
				current("set", that);
				if (!current("getProxy")) {
					current("setProxy", that.clone().css({
						opacity : .5
					}).toggleClass("proxy draggable").appendTo(container));
				}
				select(that, true);
				cursor = cartesian(e.pageX, e.pageY);
				origin = cartesian(that.offset().left, that.offset().top);
				$.extend(_current._data, {
					mousedown : cursor,
					anchor : cursor,
					origin : origin,
					offset : planarDistance(cursor, origin)
				});
			}).on("mousemove.dragndrop", function(e) {
				if (current("isSet")) {
					var that = $(current("getProxy"));
					$.extend(_current._data, {
						anchor : cartesian(e.pageX, e.pageY)
					});
					that.offset({
						left : data.anchor.X - data.offset.X,
						top : data.anchor.Y - data.offset.Y
					});
				}
			});
			// must always return the state of drag'n'drop on mouse-up.
			$(window).on("mouseup.dragndrop", function(e) {
				var that, proxy, placeholder;
				if (current("isSet")) {
					that = current();
					proxy = current("getProxy");
					if (proxy) {
						$.extend(_current._data, {
							mouseup : {
								X : proxy.offset().left,
								Y : proxy.offset().top
							}
						});
					}
					select(that, false);
					current("clear");
					that.animate({
						left : data.mouseup.X,
						top : data.mouseup.Y
					}, 100, function() {
						if (proxy) {
							proxy.remove();
							current("clearProxy");
						}
					});
				}
			}).on("keypress.dragndrop", function(e) {
				var that, keys = {
					ESCAPE : 27
				};
				switch (e.keyCode) {
				case keys.ESCAPE:
					if (current("isSet")) {
						that = current();
						current("getProxy").animate({
							left : data.origin.X,
							top : data.origin.Y
						}, 0, function() {
							$(this).remove();
							current("clearProxy");
						});
						select(that, false);
						current("clear");
					}
					break;
				}
			});
		},

		// public API method that is called to initialize the plug-in on each of
		// the selected elements, overridden with the provided optional
		// settings.
		init : function(options) {
			return this.each(function() {
				var that, data;
				that = $(this);
				data = that.data('dragndrop');
				// Extension initialization routine (only if not already done):
				methods._bind(that);
				if (!data) {
					that.data('dragndrop', {
						target : that
					});
				}
			});
		},
		// public API method called to finalize and remove all data and events
		// related to the drag'n'drop plug-in.
		destroy : function() {
			return this.each(function() {
				var $this, data;
				$this = $(this), data = $this.data('dragndrop');
				$(window).unbind('.dragndrop');
				data.object.remove();
				$this.removeData('dragndrop');
			});
		}
	};

	// Drag'n'Drop jQuery extension, using jQuery style design pattern.
	$.fn.dragndrop = function(method) {
		if (methods[method]) {
			return methods[method].apply(this, Array.prototype.slice.call(
					arguments, 1));
		} else if (typeof method === 'object' || !method) {
			return methods.init.apply(this, arguments);
		} else {
			$.error('Method ' + method + ' does not exist on jQuery.dragndrop');
		}
	};
})(jQuery);

/**
 * application.js (Main implementation)
 * <p>
 * Main application script. This module is run once the context document has
 * completely loaded to initialize the application environment and start up all
 * core client-side application modules (any external libraries and dependencies
 * that must be referenced from the owning document). In short, application
 * features are implemented here.
 */
$(document).ready(function() {
	"use strict";

	// convenient reference to the static container element element:
	var container = $("#canvas"),
	// within a plugin's immediate scope, the container is the 'this' keyword.

	// Object model for storing data about the currently selected item:
	_current = {

		// data fields encapsulated by this object:
		_data : {
			object : undefined,
			placeholder : undefined,
			proxy : undefined
		},
		// public getter/setter methods exposed to external callers:
		getPlaceholder : function() {
			return _current._data.placeholder;
		},
		setPlaceholder : function(item) {
			_current._data.placeholder = item;
		},
		getProxy : function() {
			return _current._data.proxy;
		},
		setProxy : function(item) {
			_current._data.proxy = item;
		},
		get : function() {
			return _current._data.object;
		},
		set : function(item) {
			_current._data.object = item;
		},
		isSet : function() {
			return _current._data.object !== undefined;
		}
	},

	// Shorthand function for accessing and manipulating the current item:
	current = function(i) {
		var result, args;
		if (i) {
			if (typeof i === "string") {
				if (i === "clear") {
					_current.set(undefined);
				} else if (i === "clearProxy") {
					_current.setProxy(undefined);
				} else if (i === "clearPlaceholder") {
					_current.setPlaceholder(undefined);
				} else if (typeof _current[i] === "function") {
					args = Array.prototype.slice.call(arguments, 1);
					result = _current[i].apply(this, args);
					return result;
				} else {
					console.error('The method ' + i + ' does not exist.');
				}
			} else if (typeof i === "object")
				_current.set(i);
			else
				console.error(i, "is not a valid argument.");
		}
		return _current.get();
	},

	// Data structure containing knowledge about mouse click positions.
	data = {
		mousedown : undefined,
		mouseup : undefined,
		origin : undefined,
		anchor : undefined,
		offset : undefined
	},

	// Local implementation of experimental item selection functionality.
	select = function(item, _switch) {
		var that = (item.toggleClass) ? item : $(item);
		if (that.hasClass("selected") !== _switch) {
			that.toggleClass("selected");
		}
	},

	// Convenient function for constructing Cartesian XY pairs. Can be used to
	// express planar (two-dimensional) geometric points such as coordinates,
	// or the dimensions of squares and rectangles.
	cartesianPair = function(x_Value, y_Value) {
		return {
			X : x_Value,
			Y : y_Value
		};
	},

	// Calculates the hypotenuse (diagonal intersection) given an x/y pair
	evaluateHypotenuse = function(x, y) {
		return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
	},

	// Calculates the linear distance between two Cartesian points
	linearDistance = function(pointA, pointB) {
		var dimensions = planarDistance(pointA, pointB);
		return evaluateHypotenuse(dimensions.X, dimensions.Y);
	},

	// Calculates the planar distance between two Cartesian points
	planarDistance = function(pointA, pointB) {
		return cartesianPair(pointA.X - pointB.X, pointA.Y - pointB.Y);
	},

	// Calculates the absolute planar distance between two Cartesian points
	absPlanarDistance = function(a, b) {
		return cartesianPair(Math.abs(a.X - b.X), Math.abs(a.Y - b.Y));
	};

	// delegated event bindings for dragging of items.
	container.on("mousedown.dragndrop", ".item.draggable", function(e) {
		if (!e.isDefaultPrevented()) {
			e.preventDefault();
		}
		var that, cursorLocation, itemOrigin;
		that = $(this);
		current("set", that);
		if (!current("getProxy")) {
			current("setProxy", that.clone().css({
				opacity : .5
			}).toggleClass("proxy draggable").appendTo(container));
		}
		select(that, true);
		cursorLocation = cartesianPair(e.pageX, e.pageY);
		itemOrigin = cartesianPair(that.offset().left, that.offset().top);
		$.extend(data, {
			mousedown : cursorLocation,
			anchor : cursorLocation,
			origin : itemOrigin,
			offset : planarDistance(cursorLocation, itemOrigin)
		});
	}).on("mousemove.dragndrop", function(e) {
		if (current("isSet")) {
			var that = $(current("getProxy"));
			$.extend(data, {
				anchor : cartesianPair(e.pageX, e.pageY)
			});
			that.offset({
				left : data.anchor.X - data.offset.X,
				top : data.anchor.Y - data.offset.Y
			});
		}
	});
	// must always return the state of drag'n'drop on mouse-up.
	$(window).on("mouseup.dragndrop", function(e) {
		var that, proxy, placeholder;
		if (current("isSet")) {
			that = current();
			proxy = current("getProxy");
			if (proxy) {
				$.extend(data, {
					mouseup : {
						X : proxy.offset().left,
						Y : proxy.offset().top
					}
				});
			}
			select(that, false);
			current("clear");
			that.animate({
				left : data.mouseup.X,
				top : data.mouseup.Y
			}, 100, function() {
				if (proxy) {
					proxy.remove();
					current("clearProxy");
				}
			});
		}
	});
	// delegated event bindings for control keys related to dragging.
	$(window).on("keypress.dragndrop", function(e) {
		var that, keys = {
			ESCAPE : 27
		};
		switch (e.keyCode) {
		case keys.ESCAPE:
			if (current("isSet")) {
				that = current();
				current("getProxy").animate({
					left : data.origin.X,
					top : data.origin.Y
				}, 0, function() {
					$(this).remove();
					current("clearProxy");
				});
				select(that, false);
				current("clear");
			}
			break;
		}
	});
});
